RTSP 服务器实现

RTSP 服务器实现

RTSP协议解析及实现

RTSP是一个实时传输流协议,是一个应用层的协议。通常说的RTSP 包括RTSP协议、RTP协议、RTCP协议,对于这些协议的作用简单的理解如下

  • RTSP协议:负责服务器与客户端之间的请求与响应
  • RTP协议: 负责服务器与客户端之间传输媒体数据
  • RTCP协议:负责提供有关RTP传输质量的反馈,就是确保RTP传输的质量

三者的关系: rtsp并不会发送媒体数据,只是完成服务器和客户端之间的信令交互,rtp协议负责媒体数据传输,rtcp负责rtp数据包的监视和反馈。rtp和rtcp并没有规定传输层的类型,可以选择udp和tcp。Rtsp的传输层则要求是基于tcp。

通过一个ffmpeg 客户端拉流播放一个rtsp视频流的方式,展示RTSP的交互过程。并通过Wireshark抓包分析。

1
2
3
SDP 协议(https://blog.csdn.net/uianster/article/details/125902301)
一个会话级描述
多个媒体级描述
  • main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
   // 创建socket描述符
// 启动windows socket start
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("PC Server Socket Start Up Error \n");
return -1;
}
// 启动windows socket end

int serverSockfd;

serverSockfd = createTcpSocket();
if (serverSockfd < 0)
{
WSACleanup();
printf("failed to create tcp socket\n");
return -1;
}

// 绑定端口
if (bindSocketAddr(serverSockfd, "0.0.0.0", SERVER_PORT) < 0)
{
printf("failed to bind addr\n");
return -1;
}

// 监听
if (listen(serverSockfd, 10) < 0)
{
printf("failed to listen\n");
return -1;
}

printf("%s rtsp://127.0.0.1:%d\n", __FILE__, SERVER_PORT);

// 监听连接客户端的循环
while (true) {
int clientSockfd;
char clientIp[40];
int clientPort;

clientSockfd = acceptClient(serverSockfd, clientIp, &clientPort);
if (clientSockfd < 0)
{
printf("failed to accept client\n");
return -1;
}

printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);

// 处理接受到数据后的逻辑
doClient(clientSockfd, clientIp, clientPort);
}

Wireshark 捕获

image-20240717105034848

本地客户端访问本地服务 通过网络回环地址 127.0.0.1 所以选择 Adapter for loopback traffic capture

筛选所捕获的rtsp数据如下所示

image-20240717105404085
1
2
3
4
OPTIONS: 获取服务器支持的方法
DESCIRIBE: 相应音视频流的编码格式
SETUP: 客户端通过setup请求与服务器简历建立与流媒体数据的连接
PLAY: 播放

客户端发起 OPITION 返回RTSP -> 又发起DESCRIBE -> 返回RTSP/SDP数据

发起SETUP请求 端口后加入 track0 这是为了区别建立流的通道

RTSP transport 规范不同 RTP/AVP/UDP

  • doclient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
while (true) {
int recvLen;

recvLen = recv(clientSockfd, rBuf, 2000, 0);
if (recvLen <= 0) {
break;
}

rBuf[recvLen] = '\0';
std::string recvStr = rBuf;
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
printf("%s rBuf = %s \n",__FUNCTION__,rBuf);

const char* sep = "\n";
char* line = strtok(rBuf, sep);
while (line) {

if (strstr(line, "OPTIONS") ||
strstr(line, "DESCRIBE") ||
strstr(line, "SETUP") ||
strstr(line, "PLAY")) {

if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3) {
// error
}
}
else if (strstr(line, "CSeq")) {
if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1) {
// error
}
}
else if (!strncmp(line, "Transport:", strlen("Transport:"))) {
// Transport: RTP/AVP/UDP;unicast;client_port=13358-13359
// Transport: RTP/AVP;unicast;client_port=13358-13359

if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n",
&clientRtpPort, &clientRtcpPort) != 2) {
// error
printf("parse Transport error \n");
}
}
line = strtok(NULL, sep);
}
image-20240717174743196

ffmpeg

ffmpeg 三大命令行工具

  1. ffmpeg 主要是多媒体的编解码工具 ,具体功能主要包括视频裁剪、去水印 、添加logo、提取封面、提取音视频等。
  2. ffplay 提供了音视频显示和播放相关的图像信息,音频的波形信息等,是个播放器。
  3. ffprobe 是多媒体分析工具 比如音视频的参数、媒体容器的参数信息等。可以分析媒体文件每个包的长度、包的类型、帧的信息等。

ffmpeg 命令行

视频生成图片

1
2
3
4
5
ffmpeg -i input.mp4 -r 25 -g image2.data/image%3d.jpg
image%3d.jpg 表示图片的序号为3个数字
image%d.jpg 表示图片的序号依次增加
-r 25 帧数
-f image2 格式化的格式

图片合成视频

1
2
3
4
5
6
7
8
9
10
11
12
ffmpeg -r 1 -f image2 -i data/%d.jpg -vcodec libx264 -s 640*480 g 1 -keyint_min 1 -sc_threshold 0 -pix_fmt yuv420p out.mp4
-vcodec libx264 指定合成视频的编码格式为h264
-r 1 fps等于1 (这个参数需要写在 -f 之前)
image2 指定图片的格式
-i input
-vcodec 指定视频编码格式
-s 640*480 分辨率 需要跟图片的宽高一致 不一致会报错
-g 1 GOP 长度
-keyint_min 1 关键帧间隔 keyint表示关键帧(IDR帧)间隔,这个选项表示限制IDR帧间隔最小为1帧
-sc——threshold 0 禁用场景识别,即进制自动添加IDR帧
-pix_fmt yuv420p 帧格式
-vf scale=1200:1 指定合成视频分辨率自适应宽为1280,高按照比例计算
概念补充

GOP:GOP(Group of Pictures)是指一组图像序列,它定义了视频编码的结构和压缩效率。GOP长度则表示在该组图像序列中包含的帧数。GOP的结构通常包括以下几种类型的帧:

  1. I帧(Intra-coded frame):完全独立编码,不依赖其他帧,可以作为随机访问点(例如视频的起始点)。
  2. P帧(Predictive-coded frame):使用之前的I帧或P帧作为参考来进行预测编码,压缩效率较高。
  3. B帧(Bi-directional predictive-coded frame):使用前后帧(I帧或P帧)作为参考进行预测编码,压缩效率最高,但解码复杂度也最大。

GOP长度决定了视频流中I帧的频率。例如,一个GOP长度为12的视频流表示每12帧就会有一个I帧,其余的帧可能是P帧或B帧。较短的GOP长度(例如GOP长度为1)意味着更多的I帧,从而提高视频的随机访问性能,但会增加数据量。较长的GOP长度则会减少I帧数量,提高压缩效率,但会降低随机访问性能。

摄像头推拉流&桌面推拉流

1
2
3
4
5
6
查看摄像头列表
ffmpeg -list_devices true -f dshow -i dummy
播放摄像头 FUll HD webcam 是通过查看列表的命令行获得的名称
ffplay -f dshow -i vedio=“FULL HD webcam”
查看摄像头的分辨率格式
ffmpeg -list_options true -f dshow -i video="FULL HD webcam"
摄像头推流到RTMP服务
1
2
ffmpeg -f dshow -i video="USB webcam" -vcodec libx264 -acodec aac -ar 44100 -ac 1 -r 25 -s 1920*1080 -f flv rtmp://192.168.1.3/live/desktop
RTMP 视频的码流格式是 flv
摄像头推流到RTSP (rtp over tcp)
1
ffmpeg -f dshow -i video="FULL HD webcam" -rtsp transport tcp -vcodec libx264 -preset -ultrafast -acodec libmp3lame -ar 44100 -ac 1 -r 25 -s 1920*1080 -f rtsp rtsp://192.168.1.3/live/desktop
windows桌面推流 RTSP&RTMP
1
ffmpeg -f gdigrab -i desktop -vcodec libx264 ultrafash 0acodec libmp3lame -ar 44100 -ac 1 -r 25 -s 1920*1080 -f flv rtmp://192.168.1.3/live/desktop
windows桌面推流到RTSP服务
ffmpeg基本推拉流命令
1
2
3
4
5
RTMP推流 -re表示复制本地文件 -a表示对音频进行操作 -an表示对音频进行禁用
ffmpeg -re -i input.flv -f flv -r 25 -s 1920*1080 -an "rtmp://192.168.0.200/live/test"

RTSP拉流转RTMP推流
ffmpeg -rtsp_transport tcp -i "rtsp://admin:12345678@192.168.0.2" -f flv -c:v copy -a:v copy -r 25 -s 1920*1080 "rtmp://192.168.0.200/live/test"

ffmpeg 基本操作

指定时间段录制 -c:v copy 视频文件和源文件保持一致 -c:a copy 音频文件和源文件保持一致

1
ffmpeg -i input.mp4 -c:v copy -c:a copy ss 00:30:20 out.mp4

提取h264 裸码流

1
ffmpeg -i input.mp4 -c:v copy -bsf:v h264_mp4toannexb -an out.h264

实现传输h264的RTSP服务器

image-20240717150632411

概念补充

CSRC 计数器

CSRC(Contributing Source)计数器通常用于实时传输协议(RTP, Real-time Transport Protocol)中,特别是在涉及多路音频或视频流的情况下。RTP是一种用于在网络上传输实时数据的协议,广泛应用于视频会议、IP电话、流媒体等实时通信应用中。

CSRC 计数器的概念
  • CSRC 标识符(CSRC Identifier):在 RTP 包头中,有一个字段叫做 CSRC 列表(CSRC list),用于列出所有为当前 RTP 包贡献数据的源。每个源都有一个唯一的标识符,称为 CSRC 标识符。
  • CSRC 计数器:RTP 包头中有一个字段用于存储 CSRC 标识符的数量,即 CSRC 计数器。它指示了当前 RTP 包中有多少个源贡献了数据。
CSRC 计数器的作用

CSRC 计数器的主要作用是帮助接收端了解当前 RTP 包是由哪些源生成或混合而成的。这在混合器(mixer)或中继器(translator)等场景中尤为重要,这些设备会接收多个源的流,并将它们组合成一个新的 RTP 流发送给接收端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
RTP头的结构体

struct RtpHeader
{
/* byte 0 */
uint8_t csrcLen : 4;//CSRC计数器,占4位,指示CSRC 标识符的个数。
uint8_t extension : 1;//占1位,如果X=1,则在RTP报头后跟有一个扩展报头。
uint8_t padding : 1;//填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
uint8_t version : 2;//RTP协议的版本号,占2位,当前协议版本号为2。

/* byte 1 */
uint8_t payloadType : 7;//有效载荷类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等。
uint8_t marker : 1;//标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。

/* bytes 2,3 */
uint16_t seq;//占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。接收者通过序列号来检测报文丢失情况,重新排序报文,恢复数据。

/* bytes 4-7 */
uint32_t timestamp;//占32位,时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。

/* bytes 8-11 */
uint32_t ssrc;//占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。

/*标准的RTP Header 还可能存在 0-15个特约信源(CSRC)标识符

每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源

*/
};
  • RTP 包的结构体
1
2
3
4
5
6
7
8
RTP包的结构体

struct RtpPacket
{
struct RtpHeader rtpHeader;
uint8_t payload[0];
};
// 包含一个RTP头部和RTP载荷

H264

https://blog.csdn.net/xt18971492243/article/details/123360569

一、H264压缩技术主要采用了以下几种方法对视频数据进行压缩:
帧内预测压缩,解决的是空域数据冗余问题。
帧间预测压缩(运动估计与补偿),解决的是时域数据冗徐问题。
整数离散余弦变换(DCT),将空间上的相关性变为频域上无关的数据然后进行量化。
CABAC压缩。
经过压缩后的帧分为:I帧,P帧和B帧:

I帧:关键帧,采用帧内压缩技术。
P帧:向前参考帧,在压缩时,只参考前面已经处理的帧。采用帧音压缩技术。
B帧:双向参考帧,在压缩时,它即参考前而的帧,又参考它后面的帧。采用帧间压缩技术。
除了I/P/B帧外,还有图像序列GOP。

二、h264组成
1、网络提取层 (Network Abstraction Layer,NAL)

每一张图片压缩出来的结果都是一个独立的NAL

NALU(Network Abstraction Layer Unit,网络抽象层单元)是视频编码中非常重要的概念,特别是在H.264/AVC和H.265/HEVC等现代视频编码标准中。NALU用于组织和传输编码视频数据,使其适应不同的传输网络和存储介质。一个NALU由两个主要部分组成:NAL头(NAL Header)和NAL有效载荷(NAL Payload)。

NAL头(NAL Header)

  • 包含关于NALU类型、优先级等信息。
  • 在H.264中,NAL头通常是1个字节,包含以下几个字段:
    • forbidden_zero_bit(1位):这个位必须为0,如果不为0,说明存在错误。
    • nal_ref_idc(2位):指示该NALU的优先级,0表示最低优先级,3表示最高优先级。
    • nal_unit_type(5位):指示NALU的类型,如I帧、P帧、SPS、PPS等。
  • 在H.265中,NAL头更加复杂,包含2个字节,提供更多的描述信息。

NAL有效载荷(NAL Payload)

  • 包含实际的视频编码数据,例如编码的帧数据(如I帧、P帧、B帧),SEI消息(Supplemental Enhancement Information,补充增强信息),SPS(Sequence Parameter Set,序列参数集),PPS(Picture Parameter Set,图像参数集)等。

2、视讯编码层 (Video Coding Layer,VCL)

对于形成网络提取层过程的描述

三、码流结构
H.264的功能分为两层,视频编码层(VCL)和网络提取层(NAL)。
1.VCL数据即被压缩编码后的视频数据序列。
2.在VCL数据要封装到NAL单元中之后,才可以用来传输或存储
image-20240717165223364

SPS:序列参数集,作用于一系列连续的编码图像;
PPS:图像参数集,作用于编码视频序列中一个或多个独立的图像;

用来存储分辨率等一系列信息

四、IDR帧

是一种特殊的I帧,常用于视频编码和压缩技术中,特别是在H.264/AVC和H.265/HEVC等视频编码标准中。

IDR帧的特点

  1. 独立解码:IDR帧可以独立解码,不依赖于之前的任何帧。这意味着解码器在解码IDR帧时不需要参考之前的帧,从而提供一个新的解码起点。
  2. 清除参考帧列表:当解码器遇到一个IDR帧时,它会清除之前所有的参考帧。这防止了解码器使用过时的参考帧进行错误预测,从而保证解码的一致性和准确性。
  3. 随机访问点:IDR帧通常被用作视频流中的随机访问点(Random Access Point),允许从该帧开始进行无缝播放或快进/快退等操作。

IDR帧在视频编码中的作用

  • 视频切片:IDR帧允许视频流被分割成独立的片段,每个片段可以独立解码和播放。这对于视频编辑和处理非常重要。
  • 错误恢复:在有损网络环境中(例如流媒体传输),视频数据可能会丢失或损坏。IDR帧提供了一个清晰的恢复点,从而提高了视频流的鲁棒性和稳定性。
  • 直播和点播:在直播或点播视频流中,IDR帧允许观众在任意时间点加入直播,而无需从视频的开头开始观看。

IDR帧与I帧的区别

普通I帧:完全独立,可以解码自身,不依赖任何其他帧。它可以被后续的帧引用作为参考。

IDR帧:不仅是独立解码的I帧,而且会强制清除解码器的参考帧缓存,确保后续帧不再引用IDR帧之前的帧。它用于实现视频流的切断点或关键帧,使得在流媒体中可以从该帧开始进行无缝播放或跳转。

H264 码流进行RTP封装

H.264由一个一个的NALU组成,每个NALU之间使用00 00 00 0100 00 01分隔开,每个NALU的第一次字节都有特殊的含义

NALU(Network Abstraction Layer Unit)NALU是视频流的基本单位,每个NALU包含一段编码后的视频数据。视频编码器将原始视频帧编码为NALU,然后将这些NALU打包成RTP(实时传输协议)包进行传输。

image-20240717172756235

image-20240717172801938 image-20240719144547315
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
常用Nalu_type:

0x06 (0 00 00110) SEI type = 6

0x67 (0 11 00111) SPS type = 7

0x68 (0 11 01000) PPS type = 8



0x65 (0 11 00101) IDR type = 5

0x65 (0 10 00101) IDR type = 5

0x65 (0 01 00101) IDR type = 5

0x65 (0 00 00101) IDR type = 5



0x61 (0 11 00001) I帧 type = 1

0x41 (0 10 00001) P帧 type = 1

0x01 (0 00 00001) B帧 type = 1

注:SPS和PPS只是编码解码的参数 不是视频数据

SPS 和 PPS 定义的是视频流的结构和参数,这些信息在一个视频序列中相对稳定,不需要在每个帧上进行时间同步。它们不代表具体的帧数据,而是描述解码器如何解释后续的实际帧数据。

从一个H.264的文件中将一个一个的NALU提取出来,然后封装成RTP包

1,F(forbiden):禁止位,占用NAL头的第一个位,当禁止位值为1时表示语法错误;

2,NRI:参考级别,占用NAL头的第二到第三个位;值越大,该NAL越重要。(重要级别)

3,Type:Nal单元数据类型,也就是标识该NAL单元的数据类型是哪种,占用NAL头的第四到第8个位;

  • H.264可以由三种RTP打包方式

(1)单NALU打包: 一个RTP包包含一个完整的NALU。将一整个NALU的数据放入RTP包的载荷中,这是最简单的一种方式。

(2)聚合打包:对于较小的NALU,一个RTP包可包含多个完整的NALU

(3)分片打包:对于较大的NALU,一个NALU可以分为多个RTP包发送 每个RTP包都有大小限制的,因为RTP一般都是使用UDP发送,UDP没有流量控制,所以要限制每一次发送的大小,所以如果一个NALU的太大,就需要分成多个RTP包发送,至于如何分成多个RTP包

当我们进入play method的时候,需要都H264文件,进行封包,RTCP/UDP 通道 传输

视频数据封装和传输

  • 视频编码:NALU是视频流的基本单位,每个NALU包含一段编码后的视频数据。视频编码器将原始视频帧编码为NALU,然后将这些NALU打包成RTP(实时传输协议)包进行传输。
  • 传输效率:通过将视频数据划分为多个NALU,可以提高传输效率和适应不同网络条件。例如,在不可靠的网络条件下,丢失一个NALU不会影响整个视频流的解码。

解码和播放

  • NALU解析:RTSP客户端接收RTP包后,需要解析NALU以重建原始的视频帧。解析NALU是解码过程的关键步骤,它决定了视频流的质量和流畅性。
  • 同步播放:NALU中包含时间戳信息,用于在客户端同步播放视频和音频流。正确解析这些时间戳对于实现流畅的播放体验至关重要。

错误恢复和重传

  • 错误检测:NALU的头部信息可以帮助检测传输中的错误,并根据需要进行错误恢复。RTSP服务器和客户端可以使用这些信息来判断NALU是否丢失或损坏。
  • 重传机制:如果检测到NALU丢失或损坏,RTSP服务器可以根据需要启动重传机制,以确保视频流的完整性和质量。

SSRC

SSRC(Synchronization Source identifier)是实时传输协议(RTP)中的一个重要概念。它用于标识参与 RTP 会话的各个数据源,以实现同步和管理。每个 RTP 数据包都包含一个 SSRC 标识符,用于标识数据包的源。这个标识符是随机生成的,目的是确保在同一 RTP 会话中,每个数据源都有一个唯一的标识符。

同步和多路复用

  • 在 RTP 会话中,同步多个数据流(如音频和视频)是至关重要的。SSRC 使接收端能够正确地同步这些流,确保音频和视频之间的协调一致。
  • 多路复用是指在同一 RTP 会话中传输多个数据流。通过使用不同的 SSRC 标识符,可以在同一会话中传输和管理多个数据流,而不会发生混淆。

冲突检测和解决

  • 在 RTP 会话中,如果两个不同的数据源生成了相同的 SSRC 标识符,就会发生冲突。RTP 规范提供了冲突检测和解决机制。例如,当检测到 SSRC 冲突时,数据源会生成一个新的 SSRC 标识符并继续发送数据。
  • 冲突检测机制确保了 SSRC 标识符的唯一性,从而保证了数据流的正确标识和管理。

生成

  • 当一个 RTP 会话开始时,每个数据源会随机生成一个 32 位的 SSRC 标识符。这个标识符在会话期间应保持不变,以确保数据流的一致性。

传输

  • 在每个 RTP 数据包的头部,都包含一个 SSRC 字段,用于标识该数据包的源。接收端使用这个 SSRC 标识符来区分和管理不同的数据流。
  • 例如,在视频会议中,音频和视频数据包会分别带有不同的 SSRC 标识符,接收端根据 SSRC 将音频和视频流进行同步和播放。

RTP包的格式是绝不会变的,永远是RTP头+RTP载荷

如何解决无法区分RTP 是分片打包的头尾还是中间

第一个字节位**FU Indicator**,其格式如下

IMG_256

高三位:与NALU第一个字节的高三位相同

Type:28,表示该RTP包一个分片,为什么是28

第二个字节位**FU Header**,其格式如下

IMG_256

S:标记该分片打包的第一个RTP包

E:比较该分片打包的最后一个RTP包

Type:NALU的Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//开始播放,发送RTP包
if (!strcmp(method, "PLAY")) {

int frameSize, startCode;
char* frame = (char*)malloc(500000);
struct RtpPacket* rtpPacket = (struct RtpPacket*)malloc(500000);
FILE* fp = fopen(H264_FILE_NAME, "rb");
if (!fp) {
printf("读取 %s 失败\n", H264_FILE_NAME);
break;
}
rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264, 0,
0, 0, 0x88923423);
// 0x88923423 SSRC 已写死

printf("start play\n");
printf("client ip:%s\n", clientIP);
printf("client port:%d\n", clientRtpPort);

while (true) {
frameSize = getFrameFromH264File(fp, frame, 500000);
if (frameSize < 0)
{
printf("读取%s结束,frameSize=%d \n", H264_FILE_NAME, frameSize);
break;
}

// 判断当前帧起始码
if (startCode3(frame))
startCode = 3;
else
startCode = 4;

// 帧的实际数据长度
frameSize -= startCode;
rtpSendH264Frame(serverRtpSockfd, clientIP, clientRtpPort,
rtpPacket, frame + startCode, frameSize);



Sleep(40);
//usleep(40000);//1000/25 * 1000
}
free(frame);
free(rtpPacket);

break;
}

memset(method,0,sizeof(method)/sizeof(char));
memset(url,0,sizeof(url)/sizeof(char));
CSeq = 0;

根据3 or 4个分隔符每次读取一个NALU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int getFrameFromH264File(FILE* fp, char* frame, int size) {
int rSize, frameSize;
char* nextStartCode;

if (fp < 0)
return -1;

rSize = fread(frame, 1, size, fp);

if (!startCode3(frame) && !startCode4(frame))
return -1;

nextStartCode = findNextStartCode(frame + 3, rSize - 3);
if (!nextStartCode)
{
//lseek(fd, 0, SEEK_SET);
//frameSize = rSize;
return -1;
}
else
{
frameSize = (nextStartCode - frame);
fseek(fp, frameSize - rSize, SEEK_CUR);

}

return frameSize;
}

将 H.264 视频帧分片和封装到 RTP 包中的功能,并通过 UDP 发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
static int rtpSendH264Frame(int serverRtpSockfd, const char* ip, int16_t port,
struct RtpPacket* rtpPacket, char* frame, uint32_t frameSize)
{

uint8_t naluType; // nalu第一个字节
int sendBytes = 0;
int ret;

naluType = frame[0];
// 获取后五位 naluType = frame[0] & 0x1F;

printf("frameSize=%d \n", frameSize);

if (frameSize <= RTP_MAX_PKT_SIZE) // nalu长度小于最大包长:单一NALU单元模式
{

//* 0 1 2 3 4 5 6 7 8 9
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//* |F|NRI| Type | a single NAL unit ... |
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

memcpy(rtpPacket->payload, frame, frameSize);
ret = rtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, frameSize);
if(ret < 0)
return -1;

// 加序并累计
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加时间戳
goto out;
}
else // nalu长度小于最大包场:分片模式
{

//* 0 1 2
//* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//* | FU indicator | FU header | FU payload ... |
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+



//* FU Indicator
//* 0 1 2 3 4 5 6 7
//* +-+-+-+-+-+-+-+-+
//* |F|NRI| Type |
//* +---------------+



//* FU Header
//* 0 1 2 3 4 5 6 7
//* +-+-+-+-+-+-+-+-+
//* |S|E|R| Type |
//* +---------------+


int pktNum = frameSize / RTP_MAX_PKT_SIZE; // 有几个完整的包
int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小
int i, pos = 1;

// 发送完整的包
for (i = 0; i < pktNum; i++)
{
// 2、3位与运算 后五位28或运算
rtpPacket->payload[0] = (naluType & 0x60) | 28;
rtpPacket->payload[1] = naluType & 0x1F;

if (i == 0) //第一包数据
rtpPacket->payload[1] |= 0x80; // start
else if (remainPktSize == 0 && i == pktNum - 1) //最后一包数据
rtpPacket->payload[1] |= 0x40; // end

memcpy(rtpPacket->payload+2, frame+pos, RTP_MAX_PKT_SIZE);
ret = rtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, RTP_MAX_PKT_SIZE+2);
if(ret < 0)
return -1;

rtpPacket->rtpHeader.seq++;
sendBytes += ret;
pos += RTP_MAX_PKT_SIZE;
}

// 发送剩余的数据
if (remainPktSize > 0)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28;
rtpPacket->payload[1] = naluType & 0x1F;
rtpPacket->payload[1] |= 0x40; //end

memcpy(rtpPacket->payload+2, frame+pos, remainPktSize+2);
ret = rtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, remainPktSize+2);
if(ret < 0)
return -1;

rtpPacket->rtpHeader.seq++;
sendBytes += ret;
}
}
// 时间戳累计 fps=25 时间定义1s=90000
rtpPacket->rtpHeader.timestamp += 90000 / 25;
out:

return sendBytes;

}